Alert shortcode: utilizing Quarto extensions to create a dynamic system for callouts in statistical reports

Author

Josh DeClercq

Published

April 30, 2025

1 Abstract

Statistical reports are living documents. They are usually iterative, with changes to the data, the code and presentation of results occuring over the span of the project. Communicating these changes and updates to collaborators can sometimes get lost in the suffle, e.g. email, meetings, Teams messages, etc. The more centralized the record keeping, especially when it comes to data cleaning and analysis, the better. Quarto callouts provide a good medium to highlight important, or new, information within an html report. However, callouts are more often than not a static medium. If the issue that led to the creation of a callout is resolved, it will likely be deleted in the next iteration of the report. The record of that issue–and its resolution–will fade from the document. Rather than searching through meeting minutes or emails to get a sense the evolution of the analysis, what if these echoes of previous issues could live on alongside (or within) the statistical report? The alert shortcode offers an expansion of Quarto callouts, where the creation, tracking and resolution are contained within a project-specific YAML file. This talk will provide some background on Quarto extensions and callouts, before illustrating the creation and implementation of custom Quarto shortcode.

2 Callouts

2.1 About

Callouts are an excellent way to draw extra attention to certain concepts, or to more clearly indicate that certain content is supplemental or applicable to only some scenarios.

2.2 Typical usage

::: {.callout-note}
Note that there are five types of callouts, including:
`note`, `warning`, `important`, `tip`, and `caution`.
:::

::: {.callout-tip}
## Tip with Title

This is an example of a callout with a title.
:::

::: {.callout-caution collapse="true"}
## Expand To Learn About Collapse

This is an example of a 'folded' caution callout that can be expanded by the user. You can use `collapse="true"` to collapse it by default or `collapse="false"` to make a collapsible callout that is expanded by default.
:::
Note

Note that there are five types of callouts, including: note, warning, important, tip, and caution.

Tip with Title

This is an example of a callout with a title.

This is an example of a ‘folded’ caution callout that can be expanded by the user. You can use collapse="true" to collapse it by default or collapse="false" to make a collapsible callout that is expanded by default.

2.3 Appearance

Simple appearance

Set the appearance attribute in the callout to get this look: {.callout-note appearance="simple"}

Minimal

Set the appearance attribute in the callout to get this look: {.callout-note appearance="minimal"}

Turn off icons

Set the icon attribute to false:{.callout-note icon=false}

2.4 Cross referencing

You can reference callouts anywhere in your document using a hash-ID prefix: #nte, #tip, etc. and then reference the callout using @ syntax.

::: {#tip-example .callout-tip}
## Cross-Referencing a Tip

Add an ID starting with `#tip-` to reference a tip.
:::

See @tip-example...
Tip 1: Cross-Referencing a Tip

Add an ID starting with #tip- to reference a tip.

See Tip 1

3 Quarto extensions

Extensions are a powerful way to modify and extend the behavior of Quarto.

  • Shortcodes: Special markdown directives that generate various types of content
  • Journal articles: Custom Quarto templates to mimic specific journal formats
  • Revealjs plugins: Extend the capabilities of HTML presentations created with Revealjs
  • Custom formats: Custom document templates for organization-level reporting or even formatting CVs

4 Quarto shortcodes

Special markdown directives that generate various types of content.

Some are natively supported through Quarto and can be used without any additional installation. While others can be downloaded from external sources (github).

4.1 Native shortcodes

4.1.1 meta

The meta shortcode allows you to insert content from the YAML header, or from an external YAML file _quarto.yml Can print the title of current document using {{< meta title >}}

How it’s written:

The document title is: {{< meta title >}}

How it prints:

The document title is: Alert shortcode: utilizing Quarto extensions to create a dynamic system for callouts in statistical reports

4.1.2 var

In Quarto project, the var shortcode enables you to insert content from a project-level _variables.yml file. Use it to include references to those variables within any document in your project.

Example _variables.yml:


version: 1.2

email:
  info: info@example.com
  support: support@example.com

This example uses version ?var:version. (Doesn’t work because I’m not in a Quarto Project.)

4.1.3 Lipsum

Add placeholder text to your document

{{< lipsum 1 >}}

Nullam dapibus cursus dolor sit amet consequat. Nulla facilisi. Curabitur vel nulla non magna lacinia tincidunt. Duis porttitor quam leo, et blandit velit efficitur ut. Etiam auctor tincidunt porttitor. Phasellus sed accumsan mi. Fusce ut erat dui. Suspendisse eu augue eget turpis condimentum finibus eu non lorem. Donec finibus eros eu ante condimentum, sed pharetra sapien sagittis. Phasellus non dolor ac ante mollis auctor nec et sapien. Pellentesque vulputate at nisi eu tincidunt. Vestibulum at dolor aliquam, hendrerit purus eu, eleifend massa. Morbi consectetur eros id tincidunt gravida. Fusce ut enim quis orci hendrerit lacinia sed vitae enim.

4.1.4 Placeholder

The {{< placeholder >}} shortcode generates a placeholder image:

4.2 External shortcodes

4.2.1 Font awesome

This extension provides support including free icons provided by Font Awesome.

Installation

To install, simply paste this command into your terminal from your working directory:

quarto add quarto-ext/fontawesome

Then answer a few more prompts to complete the installation. This will install the extension under the _extensions subdirectory.

Some examples

{{< fa thumbs-up >}}
{{< fa folder >}}
{{< fa chess-pawn >}}
{{< fa brands bluetooth >}}

{{< fa brands github size=5x >}}
{{< fa battery-half size=Huge >}}
{{< fa envelope title="An envelope" >}}

4.2.2 Downloadthis

Add download buttons to html files with attached small image/pdf/txt/csv files

link

4.2.3 Custom callouts

Configure a Quarto Callout with custom values such as its title, icon, icon symbol, color, appearance, and collapsibility.

link

5 Creating shortcodes

Full documentation here:

With a simple terminal command, Quarto will create templates for your shortcode extension within your chosen directory. To get started, execute quarto create extension shortcode within the parent directory.

After being prompted for a name, several files will be created:

  • README.md
  • Simple lua code example
  • a YAML file
  • .gitignore
  • An example.qmd file

Shortcodes are written in Lua and there are some references in the Quarto documentation to get you started. Or, you can prompt Chat-GPT and play around with its output to build something that works

6 Alert shortcode

6.1 Background

Oftentimes coming back into a project after months (or years) away, can leave me scratching my head on what was done and why. This is part of my efforts to address that deficiency within my reports.

  • Callouts are valuable tools for communicating updates, issues, tips, etc. to the intended audience of the report
    • Goal is to ‘alert’ reader to specific information
  • Time-sensitive callouts may get deleted as the document evolves
    • Thus losing the record of the alert and its resolution
  • Gives rise to the concept of an ‘open’ alert or ‘closed’ alerts
  • Want to keep a record of closed alerts without digging through commented-out code or version control
  • Shortcode framework points to a method of creating callouts, as well as using YAML metadata to store information

6.2 Goals

Step 1: Create shortcode Step 2: ??? Step 3: Profit $$$

  • Create a shortcode to create and track callouts using YAML metadata
  • Easy to implement and easy to update
  • Track status and view all alerts simultaneously
  • Able to be cross-referenced
  • Recycling of often used callouts to other projects

My previous callout was supposed to have newlines, but YAML formatting is tricky and my Lua code doesn’t support text formatting yet

6.3 Shortcode arguments

true/false are mostly interchangeable with yes/no in YAML, but my Lua code is not the best and so they may not be for the current version of shortcodes. Definitely something worth fixing!

Date Created: 2025-04-15

Resolved: Yes

Date Resolved: 2025-04-24

Resolution: Updated Lua code

  • [alert_name:] Each Alert must have a unique name
    • title: The title of the Callout (optional)
    • type: The standard Callout types: warning, tip, note, important, caution
    • content: The text to include in the callout
    • icon: true or false - Include the Callout icon (defaults to true)
    • collapse: true or false - Whether of not the Callout is collapsed (defaults to false)
    • date_created: Date Alert first created (Optional)
    • resolved: true or false - Whether or not the alert is resolved (defaults to no)
    • date_resolved: Date Alert resolved (Optional)
    • resolution: A description of how the alert was resolved (Optional)
    • include_extras: true or false - Whether or not to inlcude alert metadata (resolution status, dates) in the callout (defaults to false)

6.4 Within-document YAML

To access the YAML file with the alerts, you will need to add this to the YAML of the report:

metadata-files: 
  - _alerts.yml

The file can be called anything, but defaults to _alerts.yml. You can also embed the put the YAML code directly into the YAML of your report, but more than a few alerts would introduce a lot of clutter.

6.5 Examples

6.5.1 Simple callout

External YAML structure:

alerts_list:
  basic_example:
    title: "Simple callout"
    type: tip
    content: This is 'tip' callout

Alert syntax:

{{< alert "basic_example" >}}
Simple callout

This is ‘tip’ callout

6.5.2 Aside

Can use the .aside syntax to put callouts to the margins:

::: {.aside}
{{< alert "basic_example" >}}
:::
Simple callout

This is ‘tip’ callout

6.5.3 Open alert

  open_alert:
    title: Critical alert regarding age
    type: warning
    content: Very high proportion of patients are listed as 99 years old.
    icon: true
    collapse: false
    date_created: '2023-04-01'
    resolved: false
    include_extras: true
Critical alert regarding age

Very high proportion of patients are listed as 99 years old.

Date Created: 2023-04-01

Resolved: No

6.5.4 Closed alert

  closed_alert:
    title: Critical alert regarding age (closed)
    type: tip
    content: Very high proportion of patients are listed as 99 years old.
    icon: false
    collapse: true
    date_created: '2025-04-01'
    resolved: true
    date_resolved: '2025-04-05'
    resolution: Discovered that 99 was used to encode missing age at site X. Those
      values were set to missing and recalculated age using DOB to confirm remaining
      values.
    include_extras: true

Very high proportion of patients are listed as 99 years old.

Date Created: 2025-04-01

Resolved: Yes

Date Resolved: 2025-04-05

Resolution: Discovered that 99 was used to encode missing age at site X. Those values were set to missing and recalculated age using DOB to confirm remaining values.

6.5.5 Cross-linking

To crosslink an alert, you will need to update the YAML options within your document:

crossref:
  custom:
    - kind: float
      reference-prefix: Alert
      key: ale
      caption-location: top

Crosslink syntax:

:::{#ale-cross}
{{< alert "cross" >}}
:::

Callout:

Alert 1

The trouble with crosslinking is that the text becomes center-justified.

This is the reference for the alert: Alert 1

6.5.6 Cross-linking and aside

Alert 2
important alert

This is very important

This is the reference for the alert: Alert 2

6.5.7 Creating new alerts

This system of creating callouts is fairly straightforward, but can it be simpler?

It can! Using the add_alert function provides the framework to create YAML alerts.

The add_alert function takes two arguments:

  • yaml_file which is the name of the YAML file in which to create the alert. Defaults to _alerts.yml. If no YAML file exists in the working directory, one will be created.
  • alert_name: the name of the alert. If no name is supplied, the function will assign a name based on the number of alerts in the YAML file.

Calling add_alert() within the working directory of your qmd file will create a YAML file, open it within your RStudio browser and initialize the first alert like so:

alerts_list:
  alert001:
    title: ''
    type: ''
    content: ''
    icon: yes
    collapse: no
    date_created: '2025-04-14'
    resolved: no
    date_resolved: ''
    resolution: ''

The function will also print usage guidance to your console (adding in the crosslink or the aside call are both optional):

To display this alert in your document, copy and paste the following shortcode into your text editor:

::: {#ale-alert001}
Error: Alert ''alert001'' not found.
:::

The function:

add_alert <- function(yaml_file = "_alerts.yml", alert_name = NULL) {
  if (!requireNamespace("yaml", quietly = TRUE)) {
    stop("Package 'yaml' is required but not installed.")
  }
  if (!requireNamespace("rstudioapi", quietly = TRUE)) {
    stop("Package 'rstudioapi' is required but not installed.")
  }
  
  # Default template
  default_alert <- list(
    title = "",
    type = "",
    content = "",
    icon = TRUE,
    collapse = FALSE,
    date_created = I(as.character(Sys.Date())),  # <- Force it to be written as string
    resolved = FALSE,
    date_resolved = "",
    resolution = "",
    include_extras = FALSE
  )
  
  # Custom handlers to force true/false instead of yes/no
  custom_handlers <- list(
       logical = function(x) if (x) "true" else "false"
  )

  if (!file.exists(yaml_file)) {
    if (is.null(alert_name)) {
      new_alert_name <- "alert001"
    } else {
      new_alert_name <- alert_name
    }
    alerts_data <- list(alerts_list = list())
    alerts_data$alerts_list[[new_alert_name]] <- default_alert
    yaml::write_yaml(alerts_data, yaml_file, handlers = custom_handlers)
  } else {
    alerts_data <- yaml::read_yaml(yaml_file)
    if (is.null(alerts_data$alerts_list)) {
      alerts_data$alerts_list <- list()
    }
    n <- length(alerts_data$alerts_list)
    if (is.null(alert_name)) {
      new_alert_name <- paste0("alert", sprintf("%03d", n + 1))
    } else {
      new_alert_name <- alert_name
    }
    alerts_data$alerts_list[[new_alert_name]] <- default_alert
    yaml::write_yaml(alerts_data, yaml_file, handlers = custom_handlers)
  }
  ## Fix quoting or boolean madness
  ## Having such trouble with true/false and yes/no quoted and not. Bah!
  x <- readLines(yaml_file)
  x <- gsub("'true'", "true", x, fixed = TRUE)
  x <- gsub("'false'", "false", x, fixed = TRUE)
  writeLines(x, yaml_file)
  
  if (rstudioapi::isAvailable()) {
    rstudioapi::documentOpen(normalizePath(yaml_file))
  } else {
    message("RStudio API not available; please open ", yaml_file, " manually.")
  }

  cat(paste0(
    "To display this alert in your document, copy and paste the following shortcode into your text editor:\n\n",
    "::: {#ale-", new_alert_name, "}\n",
    "{{< alert '", new_alert_name, "' >}}\n",
    ":::"
  ))
}

6.6 Viewing alerts

Using the parsermd package, and a few of the functions that are found in JDmisc we can ‘find’ all of the alerts in the document and combine their location information with the YAML-encoded alert information.

The read_alerts() function will parse and format all of the alerts within the YAML file as a data frame. Use this information to create an all-in-one dashboard or alerts.

With some extra effort, we can add cross-linking into the display.

Helper functions

read_alerts_df <- function(yaml_file = "_alerts.yml") {
  # Load required packages (install if necessary)
  if (!requireNamespace("yaml", quietly = TRUE)) {
    stop("Package 'yaml' is required but not installed.")
  }
  if (!requireNamespace("purrr", quietly = TRUE)) {
    stop("Package 'purrr' is required but not installed.")
  }
  if (!requireNamespace("tibble", quietly = TRUE)) {
    stop("Package 'tibble' is required but not installed.")
  }
  
  # Read the YAML file
  alerts_data <- yaml::read_yaml(yaml_file)
  
  # Extract the alerts_list
  alerts_list <- alerts_data$alerts_list
  
  # Convert the alerts_list to a dataframe
  # Each alert becomes a row; missing fields will become NA
  df <- purrr::map_df(names(alerts_list), function(alert_id) {
    alert <- alerts_list[[alert_id]]
    alert$id <- alert_id  # add an id column from the key
    tibble::as_tibble(alert)
  })
  
  # Optional: reorder columns to have 'id' first
  df <- df[, c("id", setdiff(names(df), "id"))]
  
  return(df)
}

grep_alerts <- function(input_string){

    pattern <- "\\{\\{<\\s*alert\\s+([^ >]+)\\s*>\\}\\}"
    matches <- str_match_all(input_string, pattern)[[1]]
    alert_names <- matches[,2]
    alert_names
}
# Example usage:
alerts_df <- read_alerts_df("_alerts.yml")

parsermd::parse_rmd("shortcode_presentation.qmd") %>%as.data.frame() %>% 
    mutate(order = cumsum(type == "rmd_heading")) %>% rowwise() %>% 
    mutate(x = ifelse(type == "rmd_markdown", paste0(parsermd::as_document(ast), collapse = ""), "")) %>% 
  mutate(has_alert = grepl("\\{\\{< alert", x)) %>% 
    mutate(y = "") %>% 
    filter(order >0, has_alert) %>% 
  left_join(., get_toc("shortcode_presentation.qmd"), by= "order") %>% 
  mutate(x = gsub('\\"', "", x)) %>% 
  mutate(id = toString(grep_alerts(x))) %>% 
  separate_rows(., "id", sep = ",") %>% mutate(id= trimws(id)) %>% ungroup() %>% select(-title, -type) %>%  
  distinct() %>% 
  left_join(., alerts_df,  by = "id") %>% 
    mutate(section = case_when(is.na(section) ~ "Not included", 
                              !grepl("#ale", x) ~ glue::glue('§{section}'),
                              TRUE ~ glue::glue('<a href="#ale-{id}" class="quarto-xref" aria-expanded="false">§{section}</a>') )) %>% 
  select(section, title, resolved, content, resolution , any_of(c("date_created", "date_resolved"))) %>% 
  j.reactable(., columns = list(section = colDef(html = TRUE, width = 66),
                                title = colDef(width = 80),
                                content = colDef(width = 300),
                                resolution = colDef(width = 300)), 
              pagination = FALSE, height = 500)
# add_alert(alert_name= "test_fun")

7 Installing

Make sure github is up to date before presenting–this includes the example qmd file!

Date Created: 2025-04-15

Resolved: Yes

Date Resolved: 2025-04-24

Resolution: Fixed some lua code, redid the simple example.qmd file

This shortcode is hosted on GitHub

Within terminal, navigate to the working directory of the current project then run the command provided on the GitHub page.

Alternately, run this line and paste into terminal to add alert shortcode extension:

cat("cd ",gsub(" ", "\\\\ ", getwd()), "\nquarto add jjdeclercq/alert_shortcode")

Output from previous command:

cd  /Users/joshdeclercq/Documents/GitHub/alert_shortcode 
quarto add jjdeclercq/alert_shortcode

This will install the extension under the _extensions subdirectory. If you’re using version control, you will want to check in this directory.